Implement cfg-based target-specific dependencies
authorAlex Crichton <alex@alexcrichton.com>
Tue, 26 Jan 2016 00:54:10 +0000 (16:54 -0800)
committerAlex Crichton <alex@alexcrichton.com>
Sun, 14 Feb 2016 19:20:47 +0000 (11:20 -0800)
This commit is an implementation of [RFC 1361][rfc] which is an extension of
Cargo's `target` section in `Cargo.toml` to allow the use of `#[cfg]`-like
expressions for target-specific dependencies. Now that the compiler has been
extended with `--print cfg` each invocation of Cargo will scrape this output and
learn about the relevant `#[cfg]` directives in play for the target being
compiled. Cargo will then use these directives to decide whether a dependency
should be activated or not.

This should allow definition of dependencies along the lines of:

    [target.'cfg(unix)'.dependencies]
    [target.'cfg(target_os = "linux")'.dependencies]
    [target.'cfg(windows)'.dependencies]

Which is much more ergonomic and robust than listing all the triples out!

src/cargo/core/dependency.rs
src/cargo/ops/cargo_rustc/context.rs
src/cargo/ops/registry.rs
src/cargo/sources/registry.rs
src/cargo/util/cfg.rs [new file with mode: 0644]
src/cargo/util/mod.rs
src/cargo/util/toml.rs
src/doc/manifest.md
tests/support/registry.rs
tests/test_cargo_cfg.rs [new file with mode: 0644]
tests/tests.rs

index ce538d964a39c16d34341122187d6e8dcd287b9a..a030de626f9da2759a18caf0b4c9139b881602d7 100644 (file)
@@ -1,12 +1,22 @@
+use std::fmt;
+use std::rc::Rc;
+use std::str::FromStr;
+
 use semver::VersionReq;
 use rustc_serialize::{Encoder, Encodable};
 
 use core::{SourceId, Summary, PackageId};
-use std::rc::Rc;
-use util::CargoResult;
+use util::{CargoError, CargoResult, Cfg, CfgExpr, ChainError, human};
+
+/// Information about a dependency requested by a Cargo manifest.
+/// Cheap to copy.
+#[derive(PartialEq, Clone ,Debug)]
+pub struct Dependency {
+    inner: Rc<DependencyInner>,
+}
 
 /// The data underlying a Dependency.
-#[derive(PartialEq,Clone,Debug)]
+#[derive(PartialEq, Clone, Debug)]
 pub struct DependencyInner {
     name: String,
     source_id: SourceId,
@@ -21,17 +31,15 @@ pub struct DependencyInner {
 
     // This dependency should be used only for this platform.
     // `None` means *all platforms*.
-    only_for_platform: Option<String>,
+    platform: Option<Platform>,
 }
 
-/// Information about a dependency requested by a Cargo manifest.
-/// Cheap to copy.
-#[derive(PartialEq,Clone,Debug)]
-pub struct Dependency {
-    inner: Rc<DependencyInner>,
+#[derive(Clone, Debug, PartialEq)]
+pub enum Platform {
+    Name(String),
+    Cfg(CfgExpr),
 }
 
-
 #[derive(RustcEncodable)]
 struct SerializedDependency<'a> {
     name: &'a str,
@@ -42,7 +50,7 @@ struct SerializedDependency<'a> {
     optional: bool,
     uses_default_features: bool,
     features: &'a [String],
-    target: &'a Option<&'a str>,
+    target: Option<&'a Platform>,
 }
 
 impl Encodable for Dependency {
@@ -55,7 +63,7 @@ impl Encodable for Dependency {
             optional: self.is_optional(),
             uses_default_features: self.uses_default_features(),
             features: self.features(),
-            target: &self.only_for_platform(),
+            target: self.platform(),
         }.encode(s)
     }
 }
@@ -106,7 +114,7 @@ impl DependencyInner {
             features: Vec::new(),
             default_features: true,
             specified_req: None,
-            only_for_platform: None,
+            platform: None,
         }
     }
 
@@ -118,10 +126,10 @@ impl DependencyInner {
         self.specified_req.as_ref().map(|s| &s[..])
     }
 
-    /// If none, this dependencies must be built for all platforms.
-    /// If some, it must only be built for the specified platform.
-    pub fn only_for_platform(&self) -> Option<&str> {
-        self.only_for_platform.as_ref().map(|s| &s[..])
+    /// If none, this dependency must be built for all platforms.
+    /// If some, it must only be built for matching platforms.
+    pub fn platform(&self) -> Option<&Platform> {
+        self.platform.as_ref()
     }
 
     pub fn set_kind(mut self, kind: Kind) -> DependencyInner {
@@ -159,9 +167,9 @@ impl DependencyInner {
         self
     }
 
-    pub fn set_only_for_platform(mut self, platform: Option<String>)
-                                 -> DependencyInner {
-        self.only_for_platform = platform;
+    pub fn set_platform(mut self, platform: Option<Platform>)
+                        -> DependencyInner {
+        self.platform = platform;
         self
     }
 
@@ -230,8 +238,8 @@ impl Dependency {
 
     /// If none, this dependencies must be built for all platforms.
     /// If some, it must only be built for the specified platform.
-    pub fn only_for_platform(&self) -> Option<&str> {
-        self.inner.only_for_platform()
+    pub fn platform(&self) -> Option<&Platform> {
+        self.inner.platform()
     }
 
     /// Lock this dependency to depending on the specified package id
@@ -258,3 +266,47 @@ impl Dependency {
         self.inner.matches_id(id)
     }
 }
+
+impl Platform {
+    pub fn matches(&self, name: &str, cfg: Option<&[Cfg]>) -> bool {
+        match *self {
+            Platform::Name(ref p) => p == name,
+            Platform::Cfg(ref p) => {
+                match cfg {
+                    Some(cfg) => p.matches(cfg),
+                    None => false,
+                }
+            }
+        }
+    }
+}
+
+impl Encodable for Platform {
+    fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
+        self.to_string().encode(s)
+    }
+}
+
+impl FromStr for Platform {
+    type Err = Box<CargoError>;
+
+    fn from_str(s: &str) -> CargoResult<Platform> {
+        if s.starts_with("cfg(") && s.ends_with(")") {
+            let s = &s[4..s.len()-1];
+            s.parse().map(Platform::Cfg).chain_error(|| {
+                human(format!("failed to parse `{}` as a cfg expression", s))
+            })
+        } else {
+            Ok(Platform::Name(s.to_string()))
+        }
+    }
+}
+
+impl fmt::Display for Platform {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            Platform::Name(ref n) => n.fmt(f),
+            Platform::Cfg(ref e) => write!(f, "cfg({})", e),
+        }
+    }
+}
index c85de27c4c831114ea5739deaca806b758baa186..22025b1785b47951ae5d29aa875abaf64bb23287 100644 (file)
@@ -1,6 +1,6 @@
 use std::collections::{HashSet, HashMap};
 use std::path::{Path, PathBuf};
-use std::str;
+use std::str::{self, FromStr};
 use std::sync::Arc;
 
 use regex::Regex;
@@ -8,7 +8,7 @@ use regex::Regex;
 use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target, Profile};
 use core::{TargetKind, LibKind, Profiles, Metadata, Dependency};
 use core::dependency::Kind as DepKind;
-use util::{self, CargoResult, ChainError, internal, Config, profile};
+use util::{self, CargoResult, ChainError, internal, Config, profile, Cfg, human};
 
 use super::TargetConfig;
 use super::custom_build::{BuildState, BuildScripts};
@@ -41,16 +41,20 @@ pub struct Context<'a, 'cfg: 'a> {
     host: Layout,
     target: Option<Layout>,
     target_triple: String,
-    host_dylib: Option<(String, String)>,
-    host_staticlib: Option<(String, String)>,
-    host_exe: String,
+    target_info: TargetInfo,
+    host_info: TargetInfo,
     package_set: &'a PackageSet,
-    target_dylib: Option<(String, String)>,
-    target_staticlib: Option<(String, String)>,
-    target_exe: String,
     profiles: &'a Profiles,
 }
 
+#[derive(Clone)]
+struct TargetInfo {
+    dylib: Option<(String, String)>,
+    staticlib: Option<(String, String)>,
+    exe: String,
+    cfg: Option<Vec<Cfg>>,
+}
+
 impl<'a, 'cfg> Context<'a, 'cfg> {
     pub fn new(resolve: &'a Resolve,
                sources: &'a SourceMap<'cfg>,
@@ -62,12 +66,11 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
                profiles: &'a Profiles) -> CargoResult<Context<'a, 'cfg>> {
         let target = build_config.requested_target.clone();
         let target = target.as_ref().map(|s| &s[..]);
-        let (target_dylib, target_staticlib, target_exe) = try!(Context::filename_parts(target,
-                                                                      config));
-        let (host_dylib, host_staticlib, host_exe) = if build_config.requested_target.is_none() {
-            (target_dylib.clone(), target_staticlib.clone(), target_exe.clone())
+        let target_info = try!(Context::target_info(target, config));
+        let host_info = if build_config.requested_target.is_none() {
+            target_info.clone()
         } else {
-            try!(Context::filename_parts(None, config))
+            try!(Context::target_info(None, config))
         };
         let target_triple = target.unwrap_or_else(|| {
             &config.rustc_info().host[..]
@@ -83,12 +86,8 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
             sources: sources,
             package_set: deps,
             config: config,
-            target_dylib: target_dylib,
-            target_staticlib: target_staticlib,
-            target_exe: target_exe,
-            host_dylib: host_dylib,
-            host_staticlib: host_staticlib,
-            host_exe: host_exe,
+            target_info: target_info,
+            host_info: host_info,
             compilation: Compilation::new(config),
             build_state: Arc::new(BuildState::new(&build_config, deps)),
             build_config: build_config,
@@ -103,8 +102,8 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
 
     /// Run `rustc` to discover the dylib prefix/suffix for the target
     /// specified as well as the exe suffix
-    fn filename_parts(target: Option<&str>, cfg: &Config)
-                      -> CargoResult<(Option<(String, String)>, Option<(String, String)>, String)> {
+    fn target_info(target: Option<&str>, cfg: &Config)
+                   -> CargoResult<TargetInfo> {
         let mut process = util::process(cfg.rustc());
         process.arg("-")
                .arg("--crate-name").arg("_")
@@ -116,7 +115,18 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
         if let Some(s) = target {
             process.arg("--target").arg(s);
         };
-        let output = try!(process.exec_with_output());
+
+        let mut with_cfg = process.clone();
+        with_cfg.arg("--print=cfg");
+
+        let mut has_cfg = true;
+        let output = try!(with_cfg.exec_with_output().or_else(|_| {
+            has_cfg = false;
+            process.exec_with_output()
+        }).chain_error(|| {
+            human(format!("failed to run `rustc` to learn about \
+                           target-specific information"))
+        }));
 
         let error = str::from_utf8(&output.stderr).unwrap();
         let output = str::from_utf8(&output.stdout).unwrap();
@@ -143,13 +153,25 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
             Some((staticlib_parts[0].to_string(), staticlib_parts[1].to_string()))
         };
 
-        let exe_suffix = if nobin.is_match(error) {
+        let exe = if nobin.is_match(error) {
             String::new()
         } else {
             lines.next().unwrap().trim()
                  .split('_').skip(1).next().unwrap().to_string()
         };
-        Ok((dylib, staticlib, exe_suffix))
+
+        let cfg = if has_cfg {
+            Some(try!(lines.map(Cfg::from_str).collect()))
+        } else {
+            None
+        };
+
+        Ok(TargetInfo {
+            dylib: dylib,
+            staticlib: staticlib,
+            exe: exe,
+            cfg: cfg,
+        })
     }
 
     /// Prepare this context, ensuring that all filesystem directories are in
@@ -207,9 +229,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
     /// otherwise it corresponds to the target platform.
     fn dylib(&self, kind: Kind) -> CargoResult<(&str, &str)> {
         let (triple, pair) = if kind == Kind::Host {
-            (&self.config.rustc_info().host, &self.host_dylib)
+            (&self.config.rustc_info().host, &self.host_info.dylib)
         } else {
-            (&self.target_triple, &self.target_dylib)
+            (&self.target_triple, &self.target_info.dylib)
         };
         match *pair {
             None => bail!("dylib outputs are not supported for {}", triple),
@@ -223,9 +245,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
     /// otherwise it corresponds to the target platform.
     pub fn staticlib(&self, kind: Kind) -> CargoResult<(&str, &str)> {
         let (triple, pair) = if kind == Kind::Host {
-            (&self.config.rustc_info().host, &self.host_staticlib)
+            (&self.config.rustc_info().host, &self.host_info.staticlib)
         } else {
-            (&self.target_triple, &self.target_staticlib)
+            (&self.target_triple, &self.target_info.staticlib)
         };
         match *pair {
             None => bail!("staticlib outputs are not supported for {}", triple),
@@ -284,9 +306,9 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
     pub fn target_filenames(&self, unit: &Unit) -> CargoResult<Vec<String>> {
         let stem = self.file_stem(unit);
         let suffix = if unit.target.for_host() {
-            &self.host_exe
+            &self.host_info.exe
         } else {
-            &self.target_exe
+            &self.target_info.exe
         };
 
         let mut ret = Vec::new();
@@ -532,15 +554,15 @@ impl<'a, 'cfg> Context<'a, 'cfg> {
     fn dep_platform_activated(&self, dep: &Dependency, kind: Kind) -> bool {
         // If this dependency is only available for certain platforms,
         // make sure we're only enabling it for that platform.
-        match (dep.only_for_platform(), kind) {
-            (Some(ref platform), Kind::Host) => {
-                *platform == self.config.rustc_info().host
-            },
-            (Some(ref platform), Kind::Target) => {
-                *platform == self.target_triple
-            },
-            (None, _) => true
-        }
+        let platform = match dep.platform() {
+            Some(p) => p,
+            None => return true,
+        };
+        let (name, info) = match kind {
+            Kind::Host => (&self.config.rustc_info().host, &self.host_info),
+            Kind::Target => (&self.target_triple, &self.target_info),
+        };
+        platform.matches(name, info.cfg.as_ref().map(|cfg| &cfg[..]))
     }
 
     /// Gets a package for the given package id.
index 62b287959d26ad677bb9dcd5b0128959cb189ca3..9bbdf87127acc0ba9a2fde063f65ee83a5639f0c 100644 (file)
@@ -81,7 +81,7 @@ fn transmit(pkg: &Package, tarball: &Path, registry: &mut Registry)
             name: dep.name().to_string(),
             features: dep.features().to_vec(),
             version_req: dep.version_req().to_string(),
-            target: dep.only_for_platform().map(|s| s.to_string()),
+            target: dep.platform().map(|s| s.to_string()),
             kind: match dep.kind() {
                 Kind::Normal => "normal",
                 Kind::Build => "build",
index 94bfa7d4ee24d652e65fb2b146ba6993f58e23db..7e0ad0558e3582836a8b394ff10e7fe727546155 100644 (file)
@@ -435,6 +435,11 @@ impl<'cfg> RegistrySource<'cfg> {
             _ => Kind::Normal,
         };
 
+        let platform = match target {
+            Some(target) => Some(try!(target.parse())),
+            None => None,
+        };
+
         // Unfortunately older versions of cargo and/or the registry ended up
         // publishing lots of entries where the features array contained the
         // empty feature, "", inside. This confuses the resolution process much
@@ -445,7 +450,7 @@ impl<'cfg> RegistrySource<'cfg> {
         Ok(dep.set_optional(optional)
               .set_default_features(default_features)
               .set_features(features)
-              .set_only_for_platform(target)
+              .set_platform(platform)
               .set_kind(kind)
               .into_dependency())
     }
diff --git a/src/cargo/util/cfg.rs b/src/cargo/util/cfg.rs
new file mode 100644 (file)
index 0000000..95b20e5
--- /dev/null
@@ -0,0 +1,261 @@
+use std::str::{self, FromStr};
+use std::iter;
+use std::fmt;
+
+use util::{CargoError, CargoResult, human};
+
+#[derive(Clone, PartialEq, Debug)]
+pub enum Cfg {
+    Name(String),
+    KeyPair(String, String),
+}
+
+#[derive(Clone, PartialEq, Debug)]
+pub enum CfgExpr {
+    Not(Box<CfgExpr>),
+    All(Vec<CfgExpr>),
+    Any(Vec<CfgExpr>),
+    Value(Cfg),
+}
+
+#[derive(PartialEq)]
+enum Token<'a> {
+    LeftParen,
+    RightParen,
+    Ident(&'a str),
+    Comma,
+    Equals,
+    String(&'a str),
+}
+
+struct Tokenizer<'a> {
+    s: iter::Peekable<str::CharIndices<'a>>,
+    orig: &'a str,
+}
+
+struct Parser<'a> {
+    t: iter::Peekable<Tokenizer<'a>>,
+}
+
+impl FromStr for Cfg {
+    type Err = Box<CargoError>;
+
+    fn from_str(s: &str) -> CargoResult<Cfg> {
+        let mut p = Parser::new(s);
+        let e = try!(p.cfg());
+        if p.t.next().is_some() {
+            bail!("malformed cfg value or key/value pair")
+        }
+        Ok(e)
+    }
+}
+
+impl fmt::Display for Cfg {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            Cfg::Name(ref s) => s.fmt(f),
+            Cfg::KeyPair(ref k, ref v) => write!(f, "{} = \"{}\"", k, v),
+        }
+    }
+}
+
+impl CfgExpr {
+    pub fn matches(&self, cfg: &[Cfg]) -> bool {
+        match *self {
+            CfgExpr::Not(ref e) => !e.matches(cfg),
+            CfgExpr::All(ref e) => e.iter().all(|e| e.matches(cfg)),
+            CfgExpr::Any(ref e) => e.iter().any(|e| e.matches(cfg)),
+            CfgExpr::Value(ref e) => cfg.contains(e),
+        }
+    }
+}
+
+impl FromStr for CfgExpr {
+    type Err = Box<CargoError>;
+
+    fn from_str(s: &str) -> CargoResult<CfgExpr> {
+        let mut p = Parser::new(s);
+        let e = try!(p.expr());
+        if p.t.next().is_some() {
+            bail!("can only have one cfg-expression, consider using all() or \
+                   any() explicitly")
+        }
+        Ok(e)
+    }
+}
+
+impl fmt::Display for CfgExpr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match *self {
+            CfgExpr::Not(ref e) => write!(f, "not({})", e),
+            CfgExpr::All(ref e) => write!(f, "all({})", CommaSep(e)),
+            CfgExpr::Any(ref e) => write!(f, "any({})", CommaSep(e)),
+            CfgExpr::Value(ref e) => write!(f, "{}", e),
+        }
+    }
+}
+
+struct CommaSep<'a, T: 'a>(&'a [T]);
+
+impl<'a, T: fmt::Display> fmt::Display for CommaSep<'a, T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        for (i, v) in self.0.iter().enumerate() {
+            if i > 0 {
+                try!(write!(f, ", "));
+            }
+            try!(write!(f, "{}", v));
+        }
+        Ok(())
+    }
+}
+
+impl<'a> Parser<'a> {
+    fn new(s: &'a str) -> Parser<'a> {
+        Parser {
+            t: Tokenizer {
+                s: s.char_indices().peekable(),
+                orig: s,
+            }.peekable(),
+        }
+    }
+
+    fn expr(&mut self) -> CargoResult<CfgExpr> {
+        match self.t.peek() {
+            Some(&Ok(Token::Ident(op @ "all"))) |
+            Some(&Ok(Token::Ident(op @ "any"))) => {
+                self.t.next();
+                let mut e = Vec::new();
+                try!(self.eat(Token::LeftParen));
+                while !self.try(Token::RightParen) {
+                    e.push(try!(self.expr()));
+                    if !self.try(Token::Comma) {
+                        try!(self.eat(Token::RightParen));
+                        break
+                    }
+                }
+                if op == "all" {
+                    Ok(CfgExpr::All(e))
+                } else {
+                    Ok(CfgExpr::Any(e))
+                }
+            }
+            Some(&Ok(Token::Ident("not"))) => {
+                self.t.next();
+                try!(self.eat(Token::LeftParen));
+                let e = try!(self.expr());
+                try!(self.eat(Token::RightParen));
+                Ok(CfgExpr::Not(Box::new(e)))
+            }
+            Some(&Ok(..)) => self.cfg().map(CfgExpr::Value),
+            Some(&Err(..)) => {
+                Err(self.t.next().unwrap().err().unwrap())
+            }
+            None => bail!("expected start of a cfg expression, \
+                           found nothing"),
+        }
+    }
+
+    fn cfg(&mut self) -> CargoResult<Cfg> {
+        match self.t.next() {
+            Some(Ok(Token::Ident(name))) => {
+                let e = if self.try(Token::Equals) {
+                    let val = match self.t.next() {
+                        Some(Ok(Token::String(s))) => s,
+                        Some(Ok(t)) => bail!("expected a string, found {}",
+                                             t.classify()),
+                        Some(Err(e)) => return Err(e),
+                        None => bail!("expected a string, found nothing"),
+                    };
+                    Cfg::KeyPair(name.to_string(), val.to_string())
+                } else {
+                    Cfg::Name(name.to_string())
+                };
+                Ok(e)
+            }
+            Some(Ok(t)) => bail!("expected identifier, found {}", t.classify()),
+            Some(Err(e)) => Err(e),
+            None => bail!("expected identifier, found nothing"),
+        }
+    }
+
+    fn try(&mut self, token: Token<'a>) -> bool {
+        match self.t.peek() {
+            Some(&Ok(ref t)) if token == *t => {}
+            _ => return false,
+        }
+        self.t.next();
+        true
+    }
+
+    fn eat(&mut self, token: Token<'a>) -> CargoResult<()> {
+        match self.t.next() {
+            Some(Ok(ref t)) if token == *t => Ok(()),
+            Some(Ok(t)) => bail!("expected {}, found {}", token.classify(),
+                                 t.classify()),
+            Some(Err(e)) => Err(e),
+            None => bail!("expected {}, but cfg expr ended", token.classify()),
+        }
+    }
+}
+
+impl<'a> Iterator for Tokenizer<'a> {
+    type Item = CargoResult<Token<'a>>;
+
+    fn next(&mut self) -> Option<CargoResult<Token<'a>>> {
+        loop {
+            match self.s.next() {
+                Some((_, ' ')) => {}
+                Some((_, '(')) => return Some(Ok(Token::LeftParen)),
+                Some((_, ')')) => return Some(Ok(Token::RightParen)),
+                Some((_, ',')) => return Some(Ok(Token::Comma)),
+                Some((_, '=')) => return Some(Ok(Token::Equals)),
+                Some((start, '"')) => {
+                    while let Some((end, ch)) = self.s.next() {
+                        if ch == '"' {
+                            return Some(Ok(Token::String(&self.orig[start+1..end])))
+                        }
+                    }
+                    return Some(Err(human(format!("unterminated string in cfg"))))
+                }
+                Some((start, ch)) if is_ident_start(ch) => {
+                    while let Some(&(end, ch)) = self.s.peek() {
+                        if !is_ident_rest(ch) {
+                            return Some(Ok(Token::Ident(&self.orig[start..end])))
+                        } else {
+                            self.s.next();
+                        }
+                    }
+                    return Some(Ok(Token::Ident(&self.orig[start..])))
+                }
+                Some((_, ch)) => {
+                    return Some(Err(human(format!("unexpected character in \
+                                                   cfg `{}`, expected parens, \
+                                                   a comma, an identifier, or \
+                                                   a string", ch))))
+                }
+                None => return None
+            }
+        }
+    }
+}
+
+fn is_ident_start(ch: char) -> bool {
+    ch == '_' || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')
+}
+
+fn is_ident_rest(ch: char) -> bool {
+    is_ident_start(ch) || ('0' <= ch && ch <= '9')
+}
+
+impl<'a> Token<'a> {
+    fn classify(&self) -> &str {
+        match *self {
+            Token::LeftParen => "`(`",
+            Token::RightParen => "`)`",
+            Token::Ident(..) => "an identifier",
+            Token::Comma => "`,`",
+            Token::Equals => "`=`",
+            Token::String(..) => "a string",
+        }
+    }
+}
index c83356767aefee2e8dcb929fec798ff85c34b7e0..e2f2cb971c2a81beaf51d579c87345c66737a82b 100644 (file)
@@ -1,3 +1,4 @@
+pub use self::cfg::{Cfg, CfgExpr};
 pub use self::config::Config;
 pub use self::dependency_queue::Dependency;
 pub use self::dependency_queue::{DependencyQueue, Fresh, Dirty, Freshness};
@@ -30,8 +31,9 @@ pub mod to_url;
 pub mod toml;
 pub mod lev_distance;
 pub mod job;
+mod cfg;
 mod dependency_queue;
+mod rustc;
 mod sha256;
 mod shell_escape;
 mod vcs;
-mod rustc;
index 47b67aa0c64a582a170b947b6a6136ed6297dad8..7ff7251b2e6964804fdf2e2f3b1ed15fdc2de5c2 100644 (file)
@@ -12,7 +12,7 @@ use rustc_serialize::{Decodable, Decoder};
 use core::{SourceId, Profiles};
 use core::{Summary, Manifest, Target, Dependency, DependencyInner, PackageId,
            GitReference};
-use core::dependency::Kind;
+use core::dependency::{Kind, Platform};
 use core::manifest::{LibKind, Profile, ManifestMetadata};
 use core::package_id::Metadata;
 use util::{self, CargoResult, human, ToUrl, ToSemver, ChainError, Config};
@@ -293,6 +293,8 @@ struct Context<'a, 'b> {
     source_id: &'a SourceId,
     nested_paths: &'a mut Vec<PathBuf>,
     config: &'b Config,
+    warnings: &'a mut Vec<String>,
+    platform: Option<Platform>,
 }
 
 // These functions produce the equivalent of specific manifest entries. One
@@ -512,35 +514,30 @@ impl TomlManifest {
                 source_id: source_id,
                 nested_paths: &mut nested_paths,
                 config: config,
+                warnings: &mut warnings,
+                platform: None,
             };
 
             // Collect the deps
             try!(process_dependencies(&mut cx, self.dependencies.as_ref(),
-                                      |dep| dep, &mut warnings));
+                                      None));
             try!(process_dependencies(&mut cx, self.dev_dependencies.as_ref(),
-                                      |dep| dep.set_kind(Kind::Development), &mut warnings));
+                                      Some(Kind::Development)));
             try!(process_dependencies(&mut cx, self.build_dependencies.as_ref(),
-                                      |dep| dep.set_kind(Kind::Build), &mut warnings));
+                                      Some(Kind::Build)));
 
             if let Some(targets) = self.target.as_ref() {
                 for (name, platform) in targets.iter() {
+                    cx.platform = Some(try!(name.parse()));
                     try!(process_dependencies(&mut cx,
                                               platform.dependencies.as_ref(),
-                                              |dep| {
-                        dep.set_only_for_platform(Some(name.clone()))
-                    }, &mut warnings));
+                                              None));
                     try!(process_dependencies(&mut cx,
                                               platform.build_dependencies.as_ref(),
-                                              |dep| {
-                        dep.set_only_for_platform(Some(name.clone()))
-                           .set_kind(Kind::Build)
-                    }, &mut warnings));
+                                              Some(Kind::Build)));
                     try!(process_dependencies(&mut cx,
                                               platform.dev_dependencies.as_ref(),
-                                              |dep| {
-                        dep.set_only_for_platform(Some(name.clone()))
-                           .set_kind(Kind::Development)
-                    }, &mut warnings));
+                                              Some(Kind::Development)));
                 }
             }
         }
@@ -665,12 +662,10 @@ fn validate_bench_name(target: &TomlTarget) -> CargoResult<()> {
     }
 }
 
-fn process_dependencies<F>(cx: &mut Context,
-                           new_deps: Option<&HashMap<String, TomlDependency>>,
-                           mut f: F,
-                           warnings: &mut Vec<String>) -> CargoResult<()>
-    where F: FnMut(DependencyInner) -> DependencyInner
-{
+fn process_dependencies(cx: &mut Context,
+                        new_deps: Option<&HashMap<String, TomlDependency>>,
+                        kind: Option<Kind>)
+                        -> CargoResult<()> {
     let dependencies = match new_deps {
         Some(ref dependencies) => dependencies,
         None => return Ok(())
@@ -685,10 +680,13 @@ fn process_dependencies<F>(cx: &mut Context,
             TomlDependency::Detailed(ref details) => details.clone(),
         };
 
-        if details.version.is_none() && details.path.is_none() && details.git.is_none() {
-            warnings.push(format!("warning: dependency ({}) specified without providing a local \
-                                   path, Git repository, or version to use. This will be \
-                                   considered an error in future versions", n));
+        if details.version.is_none() && details.path.is_none() &&
+           details.git.is_none() {
+            cx.warnings.push(format!("warning: dependency ({}) specified \
+                                      without providing a local path, Git \
+                                      repository, or version to use. This will \
+                                      be considered an error in future \
+                                      versions", n));
         }
 
         let reference = details.branch.clone().map(GitReference::Branch)
@@ -711,16 +709,16 @@ fn process_dependencies<F>(cx: &mut Context,
             }
         }.unwrap_or(try!(SourceId::for_central(cx.config)));
 
-        let dep = try!(DependencyInner::parse(&n,
-                                              details.version.as_ref()
-                                                  .map(|v| &v[..]),
-                                              &new_source_id));
-        let dep = f(dep)
-                     .set_features(details.features.unwrap_or(Vec::new()))
-                     .set_default_features(details.default_features.unwrap_or(true))
-                     .set_optional(details.optional.unwrap_or(false))
-                     .into_dependency();
-        cx.deps.push(dep);
+        let version = details.version.as_ref().map(|v| &v[..]);
+        let mut dep = try!(DependencyInner::parse(&n, version, &new_source_id));
+        dep = dep.set_features(details.features.unwrap_or(Vec::new()))
+                 .set_default_features(details.default_features.unwrap_or(true))
+                 .set_optional(details.optional.unwrap_or(false))
+                 .set_platform(cx.platform.clone());
+        if let Some(kind) = kind {
+            dep = dep.set_kind(kind);
+        }
+        cx.deps.push(dep.into_dependency());
     }
 
     Ok(())
index 89e900f177c41ae9435f5a9018e0d430c8b05c2d..864794aad082e83077f6ccf4c4200758b922b4db 100644 (file)
@@ -166,19 +166,36 @@ The syntax of the requirement strings is described in the [crates.io
 guide](crates-io.html#using-cratesio-based-crates).
 
 Platform-specific dependencies take the same format, but are listed under the
-`target.$triple` section:
+`target` section. Normally Rust-like `#[cfg]` syntax will be used to define
+these sections:
 
 ```toml
-[target.x86_64-pc-windows-gnu.dependencies]
+[target.'cfg(windows)'.dependencies]
 winhttp = "0.4.0"
 
-[target.i686-unknown-linux-gnu.dependencies]
+[target.'cfg(unix)'.dependencies]
 openssl = "1.0.1"
+
+[target.'cfg(target_pointer_width = "32")'.dependencies]
 native = { path = "native/i686" }
 
-[target.x86_64-unknown-linux-gnu.dependencies]
+[target.'cfg(target_pointer_width = "64")'.dependencies]
+native = { path = "native/i686" }
+```
+
+Like with Rust, the syntax here supports the `not`, `any`, and `all` operators
+to combine various cfg name/value pairs. Note that the `cfg` syntax has only
+been available since Cargo 0.9.0 (Rust 1.8.0).
+
+In addition to `#[cfg]` syntax, Cargo also supports listing out the full target
+the dependencies would apply to:
+
+```toml
+[target.x86_64-pc-windows-gnu.dependencies]
+winhttp = "0.4.0"
+
+[target.i686-unknown-linux-gnu.dependencies]
 openssl = "1.0.1"
-native = { path = "native/x86_64" }
 ```
 
 If you’re using a custom target specification, quote the full path and file
index 63f0532453f0d537fc67fb98c82b9e025d260ab6..5452fe5837350d0933638f89cdacc5441f43c59c 100644 (file)
@@ -21,7 +21,7 @@ pub fn dl_url() -> Url { Url::from_file_path(&*dl_path()).ok().unwrap() }
 pub struct Package {
     name: String,
     vers: String,
-    deps: Vec<(String, String, &'static str)>,
+    deps: Vec<(String, String, &'static str, String)>,
     files: Vec<(String, String)>,
     yanked: bool,
 }
@@ -64,12 +64,23 @@ impl Package {
     }
 
     pub fn dep(&mut self, name: &str, vers: &str) -> &mut Package {
-        self.deps.push((name.to_string(), vers.to_string(), "normal"));
+        self.deps.push((name.to_string(), vers.to_string(), "normal",
+                        "null".to_string()));
+        self
+    }
+
+    pub fn target_dep(&mut self,
+                      name: &str,
+                      vers: &str,
+                      target: &str) -> &mut Package {
+        self.deps.push((name.to_string(), vers.to_string(), "normal",
+                        format!("\"{}\"", target)));
         self
     }
 
     pub fn dev_dep(&mut self, name: &str, vers: &str) -> &mut Package {
-        self.deps.push((name.to_string(), vers.to_string(), "dev"));
+        self.deps.push((name.to_string(), vers.to_string(), "dev",
+                        "null".to_string()));
         self
     }
 
@@ -83,14 +94,14 @@ impl Package {
         self.make_archive();
 
         // Figure out what we're going to write into the index
-        let deps = self.deps.iter().map(|&(ref name, ref req, ref kind)| {
+        let deps = self.deps.iter().map(|&(ref name, ref req, ref kind, ref target)| {
             format!("{{\"name\":\"{}\",\
                        \"req\":\"{}\",\
                        \"features\":[],\
                        \"default_features\":false,\
-                       \"target\":null,\
+                       \"target\":{},\
                        \"optional\":false,\
-                       \"kind\":\"{}\"}}", name, req, kind)
+                       \"kind\":\"{}\"}}", name, req, target, kind)
         }).collect::<Vec<_>>().connect(",");
         let cksum = {
             let mut c = Vec::new();
@@ -141,15 +152,20 @@ impl Package {
             version = "{}"
             authors = []
         "#, self.name, self.vers);
-        for &(ref dep, ref req, kind) in self.deps.iter() {
-            manifest.push_str(&format!(r#"
-                [{}dependencies.{}]
-                version = "{}"
-            "#, match kind {
+        for &(ref dep, ref req, kind, ref target) in self.deps.iter() {
+            let target = match &target[..] {
+                "null" => String::new(),
+                t => format!("target.{}.", t),
+            };
+            let kind = match kind {
                 "build" => "build-",
                 "dev" => "dev-",
                 _ => ""
-            }, dep, req));
+            };
+            manifest.push_str(&format!(r#"
+                [{}{}dependencies.{}]
+                version = "{}"
+            "#, target, kind, dep, req));
         }
 
         let dst = self.archive_dst();
diff --git a/tests/test_cargo_cfg.rs b/tests/test_cargo_cfg.rs
new file mode 100644 (file)
index 0000000..9ac9457
--- /dev/null
@@ -0,0 +1,333 @@
+use std::str::FromStr;
+use std::fmt;
+
+use cargo::util::{Cfg, CfgExpr};
+use hamcrest::assert_that;
+
+use support::{project, execs, COMPILING, UPDATING, DOWNLOADING};
+use support::registry::Package;
+
+macro_rules! c {
+    ($a:ident) => (
+        Cfg::Name(stringify!($a).to_string())
+    );
+    ($a:ident = $e:expr) => (
+        Cfg::KeyPair(stringify!($a).to_string(), $e.to_string())
+    );
+}
+
+macro_rules! e {
+    (any($($t:tt),*)) => (CfgExpr::Any(vec![$(e!($t)),*]));
+    (all($($t:tt),*)) => (CfgExpr::All(vec![$(e!($t)),*]));
+    (not($($t:tt)*)) => (CfgExpr::Not(Box::new(e!($($t)*))));
+    (($($t:tt)*)) => (e!($($t)*));
+    ($($t:tt)*) => (CfgExpr::Value(c!($($t)*)));
+}
+
+fn good<T>(s: &str, expected: T)
+    where T: FromStr + PartialEq + fmt::Debug,
+          T::Err: fmt::Display
+{
+    let c = match T::from_str(s) {
+        Ok(c) => c,
+        Err(e) => panic!("failed to parse `{}`: {}", s, e),
+    };
+    assert_eq!(c, expected);
+}
+
+fn bad<T>(s: &str, err: &str)
+    where T: FromStr + fmt::Display, T::Err: fmt::Display
+{
+    let e = match T::from_str(s) {
+        Ok(cfg) => panic!("expected `{}` to not parse but got {}", s, cfg),
+        Err(e) => e.to_string(),
+    };
+    assert!(e.contains(err), "when parsing `{}`,\n\"{}\" not contained \
+                              inside: {}", s, err, e);
+}
+
+#[test]
+fn cfg_syntax() {
+    good("foo", c!(foo));
+    good("_bar", c!(_bar));
+    good(" foo", c!(foo));
+    good(" foo  ", c!(foo));
+    good(" foo  = \"bar\"", c!(foo = "bar"));
+    good("foo=\"\"", c!(foo = ""));
+    good(" foo=\"3\"      ", c!(foo = "3"));
+    good("foo = \"3 e\"", c!(foo = "3 e"));
+}
+
+#[test]
+fn cfg_syntax_bad() {
+    bad::<Cfg>("", "found nothing");
+    bad::<Cfg>(" ", "found nothing");
+    bad::<Cfg>("\t", "unexpected character");
+    bad::<Cfg>("7", "unexpected character");
+    bad::<Cfg>("=", "expected identifier");
+    bad::<Cfg>(",", "expected identifier");
+    bad::<Cfg>("(", "expected identifier");
+    bad::<Cfg>("foo (", "malformed cfg value");
+    bad::<Cfg>("bar =", "expected a string");
+    bad::<Cfg>("bar = \"", "unterminated string");
+    bad::<Cfg>("foo, bar", "malformed cfg value");
+}
+
+#[test]
+fn cfg_expr() {
+    good("foo", e!(foo));
+    good("_bar", e!(_bar));
+    good(" foo", e!(foo));
+    good(" foo  ", e!(foo));
+    good(" foo  = \"bar\"", e!(foo = "bar"));
+    good("foo=\"\"", e!(foo = ""));
+    good(" foo=\"3\"      ", e!(foo = "3"));
+    good("foo = \"3 e\"", e!(foo = "3 e"));
+
+    good("all()", e!(all()));
+    good("all(a)", e!(all(a)));
+    good("all(a, b)", e!(all(a, b)));
+    good("all(a, )", e!(all(a)));
+    good("not(a = \"b\")", e!(not(a = "b")));
+    good("not(all(a))", e!(not(all(a))));
+}
+
+#[test]
+fn cfg_expr_bad() {
+    bad::<CfgExpr>(" ", "found nothing");
+    bad::<CfgExpr>(" all", "expected `(`");
+    bad::<CfgExpr>("all(a", "expected `)`");
+    bad::<CfgExpr>("not", "expected `(`");
+    bad::<CfgExpr>("not(a", "expected `)`");
+    bad::<CfgExpr>("a = ", "expected a string");
+    bad::<CfgExpr>("all(not())", "expected identifier");
+    bad::<CfgExpr>("foo(a)", "consider using all() or any() explicitly");
+}
+
+#[test]
+fn cfg_matches() {
+    assert!(e!(foo).matches(&[c!(bar), c!(foo), c!(baz)]));
+    assert!(e!(any(foo)).matches(&[c!(bar), c!(foo), c!(baz)]));
+    assert!(e!(any(foo, bar)).matches(&[c!(bar)]));
+    assert!(e!(any(foo, bar)).matches(&[c!(foo)]));
+    assert!(e!(all(foo, bar)).matches(&[c!(foo), c!(bar)]));
+    assert!(e!(all(foo, bar)).matches(&[c!(foo), c!(bar)]));
+    assert!(e!(not(foo)).matches(&[c!(bar)]));
+    assert!(e!(not(foo)).matches(&[]));
+    assert!(e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(bar)]));
+    assert!(e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(foo), c!(bar)]));
+
+    assert!(!e!(foo).matches(&[]));
+    assert!(!e!(foo).matches(&[c!(bar)]));
+    assert!(!e!(foo).matches(&[c!(fo)]));
+    assert!(!e!(any(foo)).matches(&[]));
+    assert!(!e!(any(foo)).matches(&[c!(bar)]));
+    assert!(!e!(any(foo)).matches(&[c!(bar), c!(baz)]));
+    assert!(!e!(all(foo)).matches(&[c!(bar), c!(baz)]));
+    assert!(!e!(all(foo, bar)).matches(&[c!(bar)]));
+    assert!(!e!(all(foo, bar)).matches(&[c!(foo)]));
+    assert!(!e!(all(foo, bar)).matches(&[]));
+    assert!(!e!(not(bar)).matches(&[c!(bar)]));
+    assert!(!e!(not(bar)).matches(&[c!(baz), c!(bar)]));
+    assert!(!e!(any((not(foo)), (all(foo, bar)))).matches(&[c!(foo)]));
+}
+
+fn setup() {}
+
+test!(cfg_easy {
+    if !::is_nightly() { return }
+
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target.'cfg(unix)'.dependencies]
+            b = { path = 'b' }
+            [target."cfg(windows)".dependencies]
+            b = { path = 'b' }
+        "#)
+        .file("src/lib.rs", "extern crate b;")
+        .file("b/Cargo.toml", r#"
+            [package]
+            name = "b"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("b/src/lib.rs", "");
+    assert_that(p.cargo_process("build").arg("-v"),
+                execs().with_status(0));
+});
+
+test!(dont_include {
+    if !::is_nightly() { return }
+
+    let other_family = if cfg!(unix) {"windows"} else {"unix"};
+    let p = project("foo")
+        .file("Cargo.toml", &format!(r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target.'cfg({})'.dependencies]
+            b = {{ path = 'b' }}
+        "#, other_family))
+        .file("src/lib.rs", "")
+        .file("b/Cargo.toml", r#"
+            [package]
+            name = "b"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("b/src/lib.rs", "");
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stdout(&format!("\
+{compiling} a v0.0.1 ([..])
+", compiling = COMPILING)));
+});
+
+test!(works_through_the_registry {
+    if !::is_nightly() { return }
+
+    Package::new("foo", "0.1.0").publish();
+    Package::new("bar", "0.1.0")
+            .target_dep("foo", "0.1.0", "cfg(unix)")
+            .target_dep("foo", "0.1.0", "cfg(windows)")
+            .publish();
+
+    let p = project("a")
+        .file("Cargo.toml", &r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            bar = "0.1.0"
+        "#)
+        .file("src/lib.rs", "extern crate bar;");
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stdout(&format!("\
+{updating} registry [..]
+{downloading} [..]
+{downloading} [..]
+{compiling} foo v0.1.0 ([..])
+{compiling} bar v0.1.0 ([..])
+{compiling} a v0.0.1 ([..])
+", compiling = COMPILING, updating = UPDATING, downloading = DOWNLOADING)));
+});
+
+test!(bad_target_spec {
+    let p = project("a")
+        .file("Cargo.toml", &r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target.'cfg(4)'.dependencies]
+            bar = "0.1.0"
+        "#)
+        .file("src/lib.rs", "");
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(101).with_stderr("\
+failed to parse manifest at `[..]`
+
+Caused by:
+  failed to parse `4` as a cfg expression
+
+Caused by:
+  unexpected character in cfg `4`, [..]
+"));
+});
+
+test!(bad_target_spec2 {
+    let p = project("a")
+        .file("Cargo.toml", &r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target.'cfg(foo =)'.dependencies]
+            bar = "0.1.0"
+        "#)
+        .file("src/lib.rs", "");
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(101).with_stderr("\
+failed to parse manifest at `[..]`
+
+Caused by:
+  failed to parse `foo =` as a cfg expression
+
+Caused by:
+  expected a string, found nothing
+"));
+});
+
+test!(multiple_match_ok {
+    if !::is_nightly() { return }
+
+    let p = project("foo")
+        .file("Cargo.toml", &format!(r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target.'cfg(unix)'.dependencies]
+            b = {{ path = 'b' }}
+            [target.'cfg(target_family = "unix")'.dependencies]
+            b = {{ path = 'b' }}
+            [target."cfg(windows)".dependencies]
+            b = {{ path = 'b' }}
+            [target.'cfg(target_family = "windows")'.dependencies]
+            b = {{ path = 'b' }}
+            [target."cfg(any(windows, unix))".dependencies]
+            b = {{ path = 'b' }}
+
+            [target.{}.dependencies]
+            b = {{ path = 'b' }}
+        "#, ::rustc_host()))
+        .file("src/lib.rs", "extern crate b;")
+        .file("b/Cargo.toml", r#"
+            [package]
+            name = "b"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("b/src/lib.rs", "");
+    assert_that(p.cargo_process("build").arg("-v"),
+                execs().with_status(0));
+});
+
+test!(any_ok {
+    if !::is_nightly() { return }
+
+    let p = project("foo")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "a"
+            version = "0.0.1"
+            authors = []
+
+            [target."cfg(any(windows, unix))".dependencies]
+            b = { path = 'b' }
+        "#)
+        .file("src/lib.rs", "extern crate b;")
+        .file("b/Cargo.toml", r#"
+            [package]
+            name = "b"
+            version = "0.0.1"
+            authors = []
+        "#)
+        .file("b/src/lib.rs", "");
+    assert_that(p.cargo_process("build").arg("-v"),
+                execs().with_status(0));
+});
index a79eb99bd51b5c3021e9324a4ed85b0c4248759f..b67e02a0ffb5e707ea127949e37050a39444cf39 100644 (file)
@@ -70,6 +70,7 @@ mod test_cargo_verify_project;
 mod test_cargo_version;
 mod test_shell;
 mod test_cargo_death;
+mod test_cargo_cfg;
 
 thread_local!(static RUSTC: Rustc = Rustc::new("rustc").unwrap());